﻿using System;
using System.IO;
using System.Net;
using System.Text;

namespace Cdp
{    

    public class CdpMessage : IDisposable
    {
        public int Version { get; private set; }
        public MessageType Type { get; private set; }
        public bool IsAcknowledge { get; set; }
        public bool IsFinalMessage { get; private set; }
        public bool IsRealTime { get; set; }
        public bool HasTimeStamp { get; set; }

        public short SequenceNumber { get; set; }
        public int TimeStamp { get; set; }
        public int MicroTimeStamp { get; set; }
        public short ContentType { get; set; }
        public short ContentLength { get; private set; }
        public byte[] Content { get; private set; }

        public CdpMessage(MessageType type)
        {
            Version = 1;
            Type = type;
            IsAcknowledge = false;
            IsRealTime = false;
            IsFinalMessage = true;
            HasTimeStamp = false;
            SequenceNumber = 0;
            TimeStamp = 0;
            MicroTimeStamp = 0;
            ContentType = 0;
            ContentLength = 0;
            Content = null;
        }

        public static CdpMessage GeneratePushMessage(int type, byte[] buffer, int offset, int count)
        {
            CdpMessage msg = new CdpMessage(MessageType.Push);
            msg.ContentType = (short)type;
            msg.ContentLength = (short)count;
            msg.Content = new byte[count];
            Array.ConstrainedCopy(buffer, offset, msg.Content, 0, count);
            return msg;
        }

        public static CdpMessage Parse(byte[] buffer, int offset, int count)
        {
            CdpMessage msg = null;
            if (count < 4)
                return null;

            using (MemoryStream ms = new MemoryStream())
            {
                ms.Write(buffer, offset, count);
                ms.Position = 0;

                using (BinaryReader br = new BinaryReader(ms, Encoding.ASCII))
                {
                    byte data = br.ReadByte();
                    if ((data & 0x20) != 0x20)
                        return null;

                    switch (data & 0x1E)
                    {
                        case 0x2:
                            msg = new CdpMessage(MessageType.HeartBeat);
                            break;
                        case 0x4:
                            msg = new CdpMessage(MessageType.RoundTripTime);
                            break;
                        case 0x6:
                            msg = new CdpMessage(MessageType.Synchronize);
                            break;
                        case 0x8:
                            msg = new CdpMessage(MessageType.Finish);
                            break;
                        case 0xA:
                            msg = new CdpMessage(MessageType.Push);
                            break;
                        case 0xC:
                            msg = new CdpMessage(MessageType.Pull);
                            break;
                        default:
                            msg = null;
                            break;
                    }

                    if (msg != null)
                    {
                        data = br.ReadByte();
                        msg.IsAcknowledge = (data & 0x80) > 0 ? true : false;
                        msg.IsFinalMessage = (data & 0x40) > 0 ? true : false;
                        msg.IsRealTime = (data & 0x20) > 0 ? true : false;
                        msg.HasTimeStamp = (data & 0x10) > 0 ? true : false;
                        
                        msg.SequenceNumber = IPAddress.NetworkToHostOrder(br.ReadInt16());
                        if (msg.HasTimeStamp)
                        {
                            msg.TimeStamp = IPAddress.NetworkToHostOrder(br.ReadInt32());
                            msg.MicroTimeStamp = IPAddress.NetworkToHostOrder(br.ReadInt32());
                        }
                        else
                        {
                            msg.TimeStamp = 0;
                            msg.MicroTimeStamp = 0;
                        }

                        if (msg.Type == MessageType.Push)
                        {
                            msg.ContentType = IPAddress.NetworkToHostOrder(br.ReadInt16());
                            msg.ContentLength = IPAddress.NetworkToHostOrder(br.ReadInt16());
                        }
                        else
                        {
                            msg.ContentType = 0;
                            msg.ContentLength = 0;
                        }

                        if (msg.ContentLength > 0)
                            msg.Content = br.ReadBytes(msg.ContentLength);
                    }
                }
            }
            return msg;
        }

        public void SetContent(int type, byte[] buffer, int offset, int count)
        {
            if(Type == MessageType.Push)
            {
                ContentType = (short)type;
                ContentLength = (short)count;
                Content = new byte[count];
                Array.ConstrainedCopy(buffer, offset, Content, 0, count);
            }
        }

        public byte[] GetBytes()
        {
            if (Version != 1)
                return null;

            byte[] buffer = new byte[4 + (HasTimeStamp ? 8 : 0) + (Type == MessageType.Push ? 4 : 0) + (Content == null ? 0 : Content.Length)];
            using (MemoryStream ms = new MemoryStream(buffer))
            {
                using (BinaryWriter bw = new BinaryWriter(ms))
                {
                    byte data = 0x0;
                    data |= 0x20;
                    switch(Type)
                    {
                        case MessageType.HeartBeat:
                            data |= 0x2;
                            break;
                        case MessageType.RoundTripTime:
                            data |= 0x4;
                            break;
                        case MessageType.Synchronize:
                            data |= 0x6;
                            break;
                        case MessageType.Finish:
                            data |= 0x8;
                            break;
                        case MessageType.Push:
                            data |= 0xA;
                            break;
                        case MessageType.Pull:
                            data |= 0xC;
                            break;
                        default:
                            buffer = null;
                            break;
                    }

                    if (buffer != null)
                    {
                        bw.Write(data);

                        data = 0x0;
                        if (IsAcknowledge)
                            data |= 0x80;
                        
                        if (IsFinalMessage)
                            data |= 0x40;

                        if (IsRealTime)
                            data |= 0x20;

                        if (HasTimeStamp)
                            data |= 0x10;
                        bw.Write(data);

                        bw.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(SequenceNumber)));

                        if (HasTimeStamp)
                        {
                            bw.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(TimeStamp)));
                            bw.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MicroTimeStamp)));
                        }

                        if (Type == MessageType.Push)
                        {
                            ContentLength = (short)Content.Length;
                            bw.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(ContentType)));
                            bw.Write(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(ContentLength)));
                            bw.Write(Content, 0, ContentLength);
                        }
                    }
                }
            }
            return buffer;
        }

        public CdpDatagram GenerateCdpDatagram(EndPoint remoteEndPoint)
        {
            CdpDatagram data = new CdpDatagram();
            data.Buffer = GetBytes();
            data.Offset = 0;
            data.BytesTransferred = data.Buffer.Length;
            data.RemoteEndPoint = remoteEndPoint;
            return data;
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("Version = ").Append(Version).Append(Environment.NewLine);
            sb.Append("Type = ").Append(Type).Append(Environment.NewLine);
            sb.Append("Is Acknowledge = ").Append(IsAcknowledge).Append(Environment.NewLine);
            sb.Append("Is Final Message = ").Append(IsFinalMessage).Append(Environment.NewLine);
            sb.Append("Is RealTime = ").Append(IsRealTime).Append(Environment.NewLine);
            sb.Append("Has TimeStamp = ").Append(HasTimeStamp).Append(Environment.NewLine);
            sb.Append("Sequence Number = ").Append(SequenceNumber).Append(Environment.NewLine);
            if (HasTimeStamp)
            {                
                sb.Append("TimeStamp = ").Append(TimeStamp).Append(Environment.NewLine);
                sb.Append("Micro TimeStamp = ").Append(MicroTimeStamp).Append(Environment.NewLine);
            }
            
            if(Type == MessageType.Push)
            {
                sb.Append("Content Length = ").Append(Content.Length).Append(Environment.NewLine);
            }
            return sb.ToString();
        }        

        public void Dispose()
        {

        }
    }

    public enum MessageType
    {
        HeartBeat,
        RoundTripTime,
        Synchronize,
        Finish,
        Push,
        Pull
    }
}
